﻿#!/usr/bin/python

#-------------------------------------------------------------------------------
# Name:        pyback
# Purpose:
#
# Author:      Trubnikov Sergey (bugrazoid@gmail.com)
#
# Created:     25.08.2013
# Copyright:   (c) Trubnikov Sergey 2013
# Licence:     GPL2
#-------------------------------------------------------------------------------

import getopt, sys, re, os, os.path, datetime, shutil, ctypes

import rsync

class Params:
    def __init__(self):
        self.source = ''
        self.destination = ''
        self.dryrun = False
        self.verbose = False
        self.log = None

class Mask:
    """This Object converts the mask (like: '1x2y') into parameters (x=1, y=2)
    on the specified keys. The result is a dictionary where 'x' and 'y' are keys"""
    def __init__(self, items):
        self.items = items
        self.results = {}

    def __str__(self):
        mask_str = ''
        for I in self.items:
            mask_str = mask_str+I+'='+self.results[I]+', '
        return mask_str

    def parse(self, mask):
        """Convert mask into parameters"""
        RE_DIGIT = re.compile(r'\d{1,}')
        n=0

        for pointer in self.items:
            param = re.search("\d{1,}"+pointer,mask) # Find item
            if param:
                point = RE_DIGIT.match(param.group()).group() # Extract digit
                if pointer == self.items[n]:
                    self.results[self.items[n]] = point
                n=n+1
            else:
                return None

        return self.results

Debug = ''

def print_and_log(message, log = None):
    print(str(message))
    if log: log.write(str(message)+"\n")

def lock_destination(dest, job_name=""):
    """Lock destination"""
    if os.path.exists(dest):
        lock_file = os.path.normpath(dest+"/is_running_lock")
        if not os.path.exists(lock_file):
            try:
                lock_flag=open(lock_file, 'w')
                lock_flag.write(job_name)
                lock_flag.close
            except IOError:
                return (4, "I/O error: "+lock_file)
        else: return (5, "")
    else: return (3, "")

    return 0

def unlock_destination(dest):
    """Unlock destination"""
    os.remove(os.path.normpath(dest+"/is_running_lock"))

def find_last_backup(dest, maxdepth, depth=0):
    """Find last backup in destination"""
    last_dir = ''
    try:
        names = os.listdir(dest)
    except os.error:
        return False
    names.sort()
    name = os.path.join(dest, names[-1])
    if os.path.isdir(name) and maxdepth<>depth:
        last_dir = find_last_backup(name, maxdepth, depth+1)
        name = ''
    else:
        return dest

    return last_dir

def create_hard_link(src, dst):
    """Create hard-link for file.
    Where:
        src - source file
        dst - new hard-link file
    """
    if os.path.isfile(src):
        if not os.path.isfile(dst):
            if ctypes.windll.kernel32.CreateHardLinkA(dst, src, 0):
                return (0, "link: "+src+" to: "+dst)
            else:
                return (3, "Can't create hard-link file '"+dst+"'")
        else:
            return (2, "File '"+dst+"' already exist")
    else:
        return (1, "File '"+src+"' is not regular file")

def create_hard_link_backup(source, dest, verbose = False, log = None):
    """Create hard-link copy of previous backup"""
    for root, dirs, files in os.walk(source):
        for name in dirs:
            os.mkdir(dest+root[len(source):]+os.sep+name)
        for name in files:
            status = create_hard_link(root+os.sep+name, dest+root[len(source):]+os.sep+name)
            if verbose or status[0]: print_and_log(status[1], log)
    return True

def print_help():
    print("usage: pyback.py options sources")
    print('''Options:
    -h, --help          Show this help screen
    -m, --mask          Set the number of backed copies
    -d, --destination   Set destination for backups
    -D, --dryrun        Dry run, without backup
    -l, --log           Set the log dir
    --debug             Debug information
    ''')

def error_exit(error_status, log = None):
    Errors = {0: "OK",
    1: "Invalid argument",
    2: "Invalid mask",
    3: "Destination not exist",
    4: "Can't create lock file",
    5: "Backup is already running",
    6: "Can't create hard-link copy",
    7: "Log directory not exist",
    8: "No sources"}
    prefix = "> "
    now = datetime.datetime.now()
    now = str(now.year).zfill(4)+'-'+str(now.month).zfill(2)+'-'+str(now.day).zfill(2)+' '+str(now.hour).zfill(2)+':'+str(now.minute).zfill(2)
    if error_status: prefix = "[err]>>> "
    print(prefix+Errors[error_status])
    log.write('['+now+']'+prefix+Errors[error_status]+"\n")
    if log: log.close
    return error_status

def list_backup(dest, maxdepth, depth=1):
    """List all backups in 'dest'"""
    list_of_dirs = []
    try:
        names = os.listdir(dest)
    except os.error:
        return False
    names.sort()
    for name in names:
        new_dest = os.path.join(dest, name)
        if maxdepth<>depth:
            list_of_dirs.extend(filter_backup(new_dest, filters, maxdepth, depth+1))
        else:
            list_of_dirs.append(new_dest)
    return list_of_dirs

def filter_backups(dest, filters, maxdepth, depth=0):
    """Detect who is who"""
    last_dir = ''
    list_of_dirs = ()
    try:
        names = os.listdir(dest)
    except os.error:
        return False
    names.sort()
    for I in xrange(len(names)):
        name = os.path.join(dest, names[I])
        if os.path.isdir(name) and maxdepth<>depth:
            last_dir = filter_backups(name, filters, maxdepth, depth+1)
##            name = ''
        else:
            return None

    list_of_dirs.append(last_dir)
    return list_of_dirs

def main():
    progress = "=>"
    print("initialization")
    global Debug
    tmp_dir = ".~temp#"
    now = datetime.datetime.now()

    # Parse params
    try:
        opts, args = getopt.getopt(sys.argv[1:],'hm:d:Dvl:', ["help","mask=",
        "destination=","dryrun","debug", "verbose", "log"])
    except getopt.GetoptError as err:
        print(str(err))
        print_help()
        return 1

    param = Params()

    for o, a in opts:
        if o in ("-h", "--help"):
            print_help()
            return 0
        elif o == "--debug":
            Debug = "[dbg]>>> "
            print_and_log(Debug+"Debug is ON", param.log)
        elif o in ("-m", "--mask"):
            mask = Mask(("m","h","d","w","M","y"))
            if not mask.parse(a):
                return 2
        elif o in ("-d", "--destination"):
            param.destination = os.path.normpath(a)
        elif o in ("-D", "--dryrun"):
            param.dryrun = True
        elif o in ("-v", "--verbose"):
            param.verbose = True
        elif o in ("-l", "--log"):
            if not os.path.exists(a):
                param.log = sys.stderr
                return 7
            else:
                param.log = os.path.join(a, str(now.year).zfill(4)+'-'+str(now.month).zfill(2)+
                            '-'+str(now.day).zfill(2)+'_'+str(now.hour).zfill(2)+'-'+str(now.minute).zfill(2))
                param.log = open(param.log, 'w')

    param.source = args
    if not param.source: return 8
    i=0
    for a in param.source:
        param.source[i] = os.path.normpath(param.source[i])
        i=i+1

    # Lock destination
    print_and_log("lock destination", param.log)
    lock_flag = lock_destination(param.destination)
    if lock_flag:
        print_and_log(lock_flag[1], param.log)
        return lock_flag[0]


    # Find last backup and copy them or create first backup
    hostname = os.getenv('COMPUTERNAME')
    tmp_dir = os.path.join(param.destination, tmp_dir)
    now = os.path.join(param.destination, hostname, str(now.year).zfill(4), str(now.month).zfill(2),
                        str(now.day).zfill(2), str(now.hour).zfill(2), str(now.minute).zfill(2))
    last_backup = find_last_backup(param.destination+os.sep+hostname, 5)

    if last_backup == now:
        print_and_log("Backup '"+now+"' already exist.", param.log)
        print_and_log("unlock destination", param.log)
        unlock_destination(param.destination)
        return 0

    if not last_backup:
        print_and_log("No backups found. Create initial backup.", param.log)
        if not os.path.exists(tmp_dir):
            os.mkdir(tmp_dir)
            print_and_log("start copy", param.log)
            for args in param.source:
                args = ["--recursive", "--delete", "--time", args, tmp_dir]
                if not param.verbose: args.insert(0, "--quiet")

                rsync.main(args)
##                print(str(args))
##                proc = subprocess.Popen(["C:\Python27\python", "-c", "import rsync; rsync.main("+str(args)+")"], stdout=subprocess.PIPE)
##                out = proc.communicate()[0]
##                out.encode('ascii', 'ignore')
##                print(out)

            print_and_log("move backup from temp dir to '"+now+"'", param.log)
            os.makedirs(now)    # Create dirs with current date
            for args in param.source:
                shutil.move(tmp_dir+os.sep+os.path.basename(args), now)
            os.rmdir(tmp_dir)
        else:
            print_and_log("It seems that the previous backup was not completed.", param.log)
    else:
        print_and_log("last backup is "+"'"+str(last_backup)+"'", param.log)
        print_and_log("create hard-link copy of previous backup", param.log)
        if not os.path.exists(tmp_dir):
            os.mkdir(tmp_dir)
            if create_hard_link_backup(last_backup, tmp_dir, param.verbose, param.log):
                # compare backup
                print_and_log("copy files from source that changed from last backup", param.log)
                for args in param.source:
                    args = ["--recursive", "--delete", "--update", "--delete", "--time", args, tmp_dir]
                    if not param.verbose: args.insert(0, "--quiet")
                    rsync.main(args)
                print_and_log("move backup from temp dir to '"+now+"'", param.log)
                os.makedirs(now)
                for args in param.source:
                    shutil.move(tmp_dir+os.sep+os.path.basename(args), now)
            else:
                print_and_log("unlock destination", param.log)
                unlock_destination(param.destination)
                os.rmdir(tmp_dir)
                return 6
            os.rmdir(tmp_dir)
        else:
            print_and_log("It seems that the previous backup was not completed.", param.log)

    # Filtering old backups
    print param.destination+os.sep+hostname
    print mask.results
    fb = filter_backups(param.destination+os.sep+hostname, mask.results, 5)
    if fb:
##        print(str(fb))
        pass


    # Show debug info
    if Debug:
        print_and_log(Debug+str(mask)+"\n"+
        Debug+"destination is '"+param.destination+"'"+"\n"+
        Debug+"source is "+str(param.source)+"\n"+
        Debug+"", param.log)

    print_and_log("unlock destination")
    unlock_destination(param.destination)

    if param.log:
        param.log.write("OK")
        param.log.close
    return 0

if __name__ == '__main__':
    sys.exit(error_exit(main(), open(os.path.normpath(os.getenv('TEMP') +os.sep+ "pyback.log"), 'a')))
